import 'dart:math';
import 'dart:async';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_settings_screens/flutter_settings_screens.dart';
import 'package:latlong2/latlong.dart';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:macless_haystack/accessory/accessory_list_item.dart';
import 'package:macless_haystack/preferences/user_preferences_model.dart';
import 'package:meta/meta.dart';

import 'dart:collection';

import 'package:provider/provider.dart';


final class CustomTileProvider extends TileProvider {
  static const chinaMinLat = 18.16;
  static const chinaMaxLat = 53.55;
  static const chinaMinLng = 73.66;
  static const chinaMaxLng = 135.05;

  /// Create a [CancellableNetworkTileProvider] to fetch tiles from the network,
  /// with cancellation support
  ///
  /// {@macro fmctp-desc}
  CustomTileProvider({
    super.headers,
    Dio? dioClient,
    this.silenceExceptions = false,
  })  : _isInternallyCreatedClient = dioClient == null,
        _dioClient = dioClient ?? Dio();

  /// Whether to ignore exceptions and errors that occur whilst fetching tiles
  /// over the network, and just return a transparent tile
  final bool silenceExceptions;

  /// Long living client used to make all tile requests by [CancellableNetworkImageProvider]
  /// for the duration that this provider is alive
  ///
  /// Not automatically closed if created externally and passed as an argument
  /// during construction.
  final Dio _dioClient;

  /// Whether [_dioClient] was created on construction (and not passed in)
  final bool _isInternallyCreatedClient;

  /// Each [Completer] is completed once the corresponding tile has finished
  /// loading
  ///
  /// Used to avoid disposing of [_dioClient] whilst HTTP requests are still
  /// underway.
  ///
  /// Does not include tiles loaded from session cache.
  final _tilesInProgress = HashMap<TileCoordinates, Completer<void>>();

  @override
  bool get supportsCancelLoading => true;

  LatLng tileXYToLatLng(int x, int y, int zoom) {
    final n = 1 << zoom;
    final lon = x / n * 360.0 - 180.0;
    final latRad = (3.141592653589793 - 2 * 3.141592653589793 * y / n);
    final lat = 180 /
        3.141592653589793 *
        (2 * atan(exp(latRad)) - 3.141592653589793 / 2);
    return LatLng(lat, lon);
  }

  bool isTileInChina(int x, int y, int z) {
    final lt = tileXYToLatLng(x, y, z);
    final rt = tileXYToLatLng(x + 1, y, z);
    final rb = tileXYToLatLng(x + 1, y + 1, z);
    final lb = tileXYToLatLng(x, y + 1, z);

    return locationModel.isInChina(lt.latitude, lt.longitude) ||
      locationModel.isInChina(rt.latitude, rt.longitude) ||
      locationModel.isInChina(rb.latitude, rb.longitude) ||
      locationModel.isInChina(lb.latitude, lb.longitude);
  }

  @override
  ImageProvider getImageWithCancelLoadingSupport(TileCoordinates coordinates,
      TileLayer options, Future<void> cancelLoading) {
    final x = coordinates.x.toInt();
    final y = coordinates.y.toInt();
    final z = coordinates.z.toInt();

    final userAmap = Settings.getValue<bool>(amapOnlyKey, defaultValue: false)! || isTileInChina(x, y, z);

    final urlTemplate = userAmap
        ? 'https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
        : 'https://server.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}';

    final url = urlTemplate
        .replaceAll('{x}', x.toString())
        .replaceAll('{y}', y.toString())
        .replaceAll('{z}', z.toString());

    // return NetworkImage(url);
    return _CancellableNetworkImageProvider(
      url: url,
      fallbackUrl: getTileFallbackUrl(coordinates, options),
      headers: headers,
      dioClient: _dioClient,
      cancelLoading: cancelLoading,
      silenceExceptions: silenceExceptions,
      startedLoading: () => _tilesInProgress[coordinates] = Completer(),
      finishedLoadingBytes: () {
        _tilesInProgress[coordinates]?.complete();
        _tilesInProgress.remove(coordinates);
      },
    );
  }

  @override
  Future<void> dispose() async {
    if (_tilesInProgress.isNotEmpty) {
      await Future.wait(_tilesInProgress.values.map((c) => c.future));
    }
    if (_isInternallyCreatedClient) _dioClient.close();
    super.dispose();
  }
}

class _CancellableNetworkImageProvider
    extends ImageProvider<_CancellableNetworkImageProvider> {
  final String url;
  final String? fallbackUrl;
  final Map<String, String> headers;
  final Dio dioClient;
  final Future<void> cancelLoading;
  final bool silenceExceptions;
  final void Function() startedLoading;
  final void Function() finishedLoadingBytes;

  const _CancellableNetworkImageProvider({
    required this.url,
    required this.fallbackUrl,
    required this.headers,
    required this.dioClient,
    required this.cancelLoading,
    required this.silenceExceptions,
    required this.startedLoading,
    required this.finishedLoadingBytes,
  });

  @override
  ImageStreamCompleter loadImage(
    _CancellableNetworkImageProvider key,
    ImageDecoderCallback decode,
  ) =>
      MultiFrameImageStreamCompleter(
        codec: _load(key, decode),
        scale: 1,
        debugLabel: url,
        informationCollector: () => [
          DiagnosticsProperty('URL', url),
          DiagnosticsProperty('Fallback URL', fallbackUrl),
          DiagnosticsProperty('Current provider', key),
        ],
      );

  Future<Codec> _load(
    _CancellableNetworkImageProvider key,
    ImageDecoderCallback decode, {
    bool useFallback = false,
  }) {
    startedLoading();

    final cancelToken = CancelToken();
    unawaited(cancelLoading.then((_) => cancelToken.cancel()));

    return dioClient
        .getUri<Uint8List>(
          Uri.parse(useFallback ? fallbackUrl ?? '' : url),
          cancelToken: cancelToken,
          options: Options(headers: headers, responseType: ResponseType.bytes),
        )
        .whenComplete(finishedLoadingBytes)
        .then((response) => ImmutableBuffer.fromUint8List(response.data!))
        .then(decode)
        .onError<Exception>((err, stack) {
      scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key));
      if (err is DioException && CancelToken.isCancel(err)) {
        return ImmutableBuffer.fromUint8List(TileProvider.transparentImage)
            .then(decode);
      }
      if (useFallback || fallbackUrl == null) {
        if (!silenceExceptions) throw err;
        return ImmutableBuffer.fromUint8List(TileProvider.transparentImage)
            .then(decode);
      }
      return _load(key, decode, useFallback: true);
    });
  }

  @override
  SynchronousFuture<_CancellableNetworkImageProvider> obtainKey(
    ImageConfiguration configuration,
  ) =>
      SynchronousFuture(this);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      (other is _CancellableNetworkImageProvider &&
          fallbackUrl == null &&
          url == other.url);

  @override
  int get hashCode =>
      Object.hashAll([url, if (fallbackUrl != null) fallbackUrl]);
}
